iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Kubernetes

不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA系列 第 10

Day 10|不再部署 nginx:完整實戰 自己寫 FastAPI、Build Image,再丟進 k8s

  • 分享至 

  • xImage
  •  

來到 Day 10 啦!

完成今天的內容後,這個系列也正式走完三分之一了。

前面幾天,我們一直使用 Nginx 當作練習用的 Container。原因很簡單:剛開始學習 Kubernetes 時,最重要的是先理解 Pod、Deployment、Service、Label 與 Selector 之間的關係。如果一開始就加入自己撰寫的 Application,當服務無法啟動時,我們可能很難立刻判斷問題到底出在程式碼、Docker Image,還是 Kubernetes 設定。

因此前面的策略是:

先專心學 Kubernetes
不要同時 Debug Application

不過從今天開始,我們要跨出重要的一步:不再部署別人準備好的 Nginx Image,而是自己撰寫一個 FastAPI 服務、建立 Docker Image,再將它部署到 Kubernetes。

今天完成的第一版架構如下:

Client
  ↓
Kubernetes Service
  ↓
FastAPI Pod

目前 Application 還很簡單,但之後我們會逐步加入 Redis、PostgreSQL、健康檢查、設定管理與其他功能,慢慢把它擴充成真正的微服務系統。


今天要完成什麼?

今天會完整走過以下流程:

撰寫 Python 程式
        ↓
建立 Docker Image
        ↓
將 Image 載入 kind
        ↓
建立 Kubernetes Deployment
        ↓
Deployment 建立 ReplicaSet
        ↓
ReplicaSet 建立 Pod
        ↓
Service 將流量導向 Pod
        ↓
使用 port-forward 從 Mac 存取服務

這條流程非常重要。未來不論你部署的是 Python、Java、Go 或 Node.js 服務,基本概念都不會有太大差異。


建立專案目錄

假設目前的專案結構如下:

cka-project/
├── app/
└── k8s/

其中:

app/

用來放 FastAPI 程式、Python 套件清單與 Dockerfile。

k8s/

則用來存放 Kubernetes YAML。

今天完成後,專案結構會變成:

cka-project/
├── app/
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
└── k8s/
    └── 01-api.yaml

如果目錄還不存在,可以先建立:

mkdir -p app k8s

接下來的指令,除非特別說明,都會假設你位於專案根目錄,也就是 cka-project/

可以使用以下指令確認目前位置:

pwd

建立 FastAPI Application

首先建立:

app/main.py

內容如下:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {
        "message": "Hello from Kubernetes"
    }


@app.get("/health/live")
def live():
    return {
        "status": "alive"
    }

這是一個非常精簡的 FastAPI Application,但它已經是一個可以接收 HTTP Request 並回傳 JSON 的 Web API。


理解這段 FastAPI 程式

第一行:

from fastapi import FastAPI

代表我們從 fastapi 套件中匯入 FastAPI 類別。

接著建立 Application 物件:

app = FastAPI()

這個 app 就是整個 Web Application 的核心。稍後啟動 Uvicorn 時,我們會告訴 Uvicorn:

請到 main.py 裡找到 app 這個物件

因此啟動指令才會寫成:

uvicorn main:app

其中:

main

代表 main.py

app

代表 main.py 裡的 app = FastAPI()

合起來就是:

main.py 裡面的 app 物件

建立首頁 API

這一段定義首頁 API:

@app.get("/")
def root():
    return {
        "message": "Hello from Kubernetes"
    }

@app.get("/") 表示:

當 Application 收到對 / 發出的 HTTP GET Request 時
執行下面的 root() 函式

例如使用者送出:

GET /

FastAPI 就會執行:

def root():

然後將 Python Dictionary:

{
    "message": "Hello from Kubernetes"
}

自動轉換成 JSON Response:

{
  "message": "Hello from Kubernetes"
}

所以我們不需要自己處理 JSON 序列化,也不需要手動設定 Content-Type: application/json,FastAPI 會替我們完成。


建立健康檢查 API

第二個 Endpoint 是:

@app.get("/health/live")
def live():
    return {
        "status": "alive"
    }

當我們訪問:

/health/live

Application 會回傳:

{
  "status": "alive"
}

目前這個 API 看起來很普通,但之後它會提供給 Kubernetes 的 Liveness Probe 使用。

Liveness Probe 的目的,是讓 Kubernetes 定期確認 Container 裡的 Application 是否仍然正常運作。

未來 Kubernetes 可能會定期發送:

GET /health/live

如果 Application 持續無法回應,Kubernetes 就可以判斷 Container 可能已經失去正常服務能力,並重新啟動它。

今天先準備好 Endpoint,之後再正式加入 Probe 設定。


建立 requirements.txt

接著建立:

app/requirements.txt

內容如下:

fastapi
uvicorn[standard]

這個檔案列出 Application 執行時需要安裝的 Python 套件。

fastapi 是 Web Framework,負責:

  • 定義 API 路徑
  • 接收 HTTP Request
  • 產生 HTTP Response
  • 將 Python 資料轉換成 JSON
  • 提供資料驗證與 API 文件等功能

不過 FastAPI 本身不是實際監聽 Port 的 HTTP Server,因此我們還需要 Uvicorn。

uvicorn[standard]

Uvicorn 是一個 ASGI Server,負責真正啟動 Server、監聽 Port,並將收到的 HTTP Request 交給 FastAPI 處理。

兩者可以簡單理解成:

Uvicorn
負責接收網路請求

FastAPI
負責決定如何處理請求

在正式專案中,通常會固定套件版本,避免套件更新後產生非預期差異,例如:

fastapi==指定版本
uvicorn[standard]==指定版本

不過目前是學習環境,先使用簡單版本即可。


建立 Dockerfile

app/ 目錄中建立:

app/Dockerfile

內容如下:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

COPY main.py .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile 描述了 Docker 應該如何將我們的程式碼製作成 Container Image。

接下來逐一理解每一個指令。


FROM:選擇 Base Image

FROM python:3.12-slim

建立 Docker Image 時,我們通常不會從完全空白的環境開始,而是使用既有的 Base Image。

這裡使用的是 Python 官方提供的:

python:3.12-slim

其中:

python

代表官方 Python Image。

3.12

代表 Python 版本。

slim

表示這是一個相對精簡的 Debian-based Image。它保留執行 Python 所需的基本環境,但移除了許多目前用不到的系統套件,因此 Image 通常比完整版本更小。

使用 Base Image 後,我們不需要自己安裝作業系統與 Python,可以直接開始安裝 Application 需要的套件。


WORKDIR:指定工作目錄

WORKDIR /app

這行會在 Image 裡設定主要工作目錄:

/app

後續的 COPYRUNCMD 等指令,都會以這個目錄作為主要執行位置。

如果 /app 不存在,Docker 會自動建立它。

可以把它理解成在 Container 裡執行:

cd /app

因此後面這一行:

COPY requirements.txt .

最後會將檔案放到:

/app/requirements.txt

這裡的 . 指的是目前的 WORKDIR,也就是 /app


第一個 COPY:複製套件清單

COPY requirements.txt .

這行會將 Build Context 中的:

requirements.txt

複製到 Image 裡的:

/app/requirements.txt

要注意,COPY 左邊是本機 Build Context 裡的檔案,右邊則是 Image 裡的路徑。

稍後我們會執行:

docker build -t cka-api:v1 ./app

最後的 ./app 就是 Build Context。

因此 Docker 能看見的是:

app/
├── main.py
├── requirements.txt
└── Dockerfile

而不是整個專案裡的所有檔案。


RUN:在 Build 階段安裝套件

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

RUN 表示這個指令會在建立 Image 的過程中執行。

它實際執行的內容相當於:

pip install --no-cache-dir -r requirements.txt

其中:

-r requirements.txt

表示依照 requirements.txt 的內容安裝套件。

--no-cache-dir

表示不要保留 pip 下載套件時產生的快取,以減少 Image 中不必要的檔案與容量。

這一步完成後,FastAPI 與 Uvicorn 就會被安裝進 Image。


為什麼先複製 requirements.txt?

Dockerfile 沒有一開始就把所有程式碼全部複製進去,而是先執行:

COPY requirements.txt .

RUN pip install \
    --no-cache-dir \
    -r requirements.txt

最後才複製:

COPY main.py .

這與 Docker Layer Cache 有關。

Dockerfile 中的每個步驟都可能形成一層快取。如果我們只修改 main.py,但沒有修改 requirements.txt,Docker 就有機會重複使用先前安裝 Python 套件的 Layer,不必每次重新下載與安裝所有套件。簡單說就是 requirements.txt 改動機率較低,所以讓他位在改動可能更平凡的 main.py 上面,想讓 Build Image 加快。

概念如下:

requirements.txt 沒有改
        ↓
重複使用 pip install 的快取
        ↓
只重新複製新的 main.py
        ↓
Build 速度更快

這是撰寫 Dockerfile 時很常見的優化方式。


第二個 COPY:複製 Application

COPY main.py .

這行會把本機的:

app/main.py

複製到 Image 裡的:

/app/main.py

此時 Image 裡的重要內容大致如下:

/app/
├── main.py
└── requirements.txt

同時 Python 環境中也已經安裝 FastAPI 與 Uvicorn。


EXPOSE:說明 Application 使用的 Port

EXPOSE 8000

這表示這個 Container 預期會在 Port 8000 提供服務。

不過要特別注意:

EXPOSE 不會自動把 Container Port 開放到 Mac

它比較像是 Image 提供的說明資訊,告訴使用者與相關工具:

這個 Application 預計使用 8000 Port

如果使用 Docker 在本機執行,仍然需要使用 -p 映射 Port:

docker run -p 8000:8000 cka-api:v1

如果部署到 Kubernetes,也還是要在 Deployment 與 Service 中設定相應的 Port。


CMD:Container 啟動時執行的指令

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

這是 Container 啟動時預設執行的指令,等同於:

uvicorn main:app \
  --host 0.0.0.0 \
  --port 8000

其中:

main:app

表示載入 main.py 裡的 app 物件。

--host 0.0.0.0

表示接受從 Container 外部進來的連線。

如果只監聽:

127.0.0.1

服務可能只能從 Container 自己內部存取,其他 Container 或 Kubernetes Service 無法連入。因此在 Container 環境中,通常要監聽:

0.0.0.0

最後:

--port 8000

表示 Uvicorn 會監聽 Container 的 Port 8000。


RUNCMD 的差別

這兩個指令非常容易混淆,一定要分清楚。

RUN (Image 建立時執行)

RUN pip install ...

是在建立 Image 時執行。

CMD (Container 啟動時執行)

CMD ["uvicorn", ...]

是在 Container 啟動時執行。

可以記成:

RUN
= Build Image 時執行

CMD
= Run Container 時執行

例如:

docker build
    ↓
執行 RUN
    ↓
產生 Image

之後:

docker run
    ↓
建立 Container
    ↓
執行 CMD

當 Kubernetes 建立 Pod 並啟動 Container 時,也會執行 Image 裡設定的 CMD


建立 Docker Image

確認目前位於 app 目錄(含有 Dockerfile 的位置),然後執行:

docker build -t cka-api:v1 .

https://ithelp.ithome.com.tw/upload/images/20260908/201685378XXGIN14DR.png

完整意思是:

使用目前目錄 . 作為 Build Context
讀取目前目錄下的 ./Dockerfile
建立一個名為 cka-api:v1 的 Image

其中:

-t

--tag 的縮寫,用來替 Image 設定名稱與 Tag。

這次的 Image 名稱是:

cka-api

Tag 是:

v1

完整 Image Reference 為:

cka-api:v1

Tag 可以用來區分不同版本,例如:

cka-api:v1
cka-api:v2
cka-api:v3

建立完成後,可以查看本機 Image:

docker images

https://ithelp.ithome.com.tw/upload/images/20260908/201685371NXu05v66f.png


先使用 Docker 測試 Image

在放進 Kubernetes 之前,建議先確認 Docker Image 本身能不能正常運作。

執行:

docker run --rm -p 8000:8000 cka-api:v1

https://ithelp.ithome.com.tw/upload/images/20260908/20168537lBpWoeb5G3.png

打開 http://0.0.0.0:8000/ 便可看到我們先前撰寫的 {"message":"Hello from k8s"} 出現在網頁上

https://ithelp.ithome.com.tw/upload/images/20260908/20168537dHgJx250Ve.png
其中:

--rm

表示 Container 停止後自動刪除,避免留下不需要的 Container。

-p 8000:8000

表示:

Mac Port 8000
        ↓
Container Port 8000

開啟另一個 Terminal,測試首頁:

curl http://localhost:8000

https://ithelp.ithome.com.tw/upload/images/20260908/20168537LeBxvFtK2f.png

應該得到:

{"message":"Hello from Kubernetes"}

再測試健康檢查:

curl http://localhost:8000/health/live

https://ithelp.ithome.com.tw/upload/images/20260908/20168537qRnXEkMlZy.png

應該得到:

{"status":"alive"}

如果這個階段就無法成功,代表問題通常出在:

  • Python 程式
  • requirements.txt
  • Dockerfile
  • Docker Image

此時先不要急著部署 Kubernetes,否則會同時增加排查範圍。

測試完成後,可以在執行 Container 的 Terminal 按下:

Control + C

停止 Container。


為什麼 kind 找不到 Mac 上的 Image?

我們剛剛建立的:

cka-api:v1

目前存在於 Mac 上的 Docker 環境。

但是 kind 建立 Kubernetes Cluster 的方式比較特別:kind 的名稱來自 Kubernetes IN Docker,它會使用 Docker Container 模擬 Kubernetes Node。

也就是說,目前其實有兩個不同的範圍:

Mac 的 Docker Image

以及:

kind Node 使用的 Container Runtime Image

即使 Mac 上執行:

docker images

看得到 cka-api:v1,也不代表 kind Node 裡一定能直接使用它。

如果 Kubernetes 找不到本機 Image,它可能會嘗試前往外部 Registry 下載:

cka-api:v1

但這個 Image 並沒有上傳到 Docker Hub 或其他 Registry,因此 Pod 可能會出現:

ErrImagePull

或:

ImagePullBackOff

所以我們需要將 Image 載入 kind Cluster。


將 Image 載入 kind

執行:

kind load docker-image \
  cka-api:v1 \
  --name cka-lab

https://ithelp.ithome.com.tw/upload/images/20260908/201685379W299JqyBx.png

這個指令表示:

將 Mac 上的 cka-api:v1
載入名為 cka-lab 的 kind Cluster

其中:

--name cka-lab

必須與建立 kind Cluster 時使用的 Cluster Name 相同。

可以先查看目前有哪些 kind Cluster:

kind get clusters

https://ithelp.ithome.com.tw/upload/images/20260908/20168537UWQ84QRO81.png

如果結果包含:

cka-lab

就代表 Cluster 名稱正確。

每次重新 Build 新版本的本機 Image 後,都要記得重新執行 kind load docker-image,否則 kind Node 可能仍然使用舊 Image。


建立 FastAPI Deployment 與 Service

接著建立:

k8s/01-api.yaml

先直接在同一份yaml 寫了兩個k8s resource:Deployment、Service
中間使用:--- 分隔不同 Resource。

內容如下:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: api
  namespace: cka-lab

spec:
  replicas: 2

  selector:
    matchLabels:
      app: api

  template:
    metadata:
      labels:
        app: api

    spec:
      containers:
        - name: api
          image: cka-api:v1
          imagePullPolicy: IfNotPresent

          ports:
            - containerPort: 8000

---

apiVersion: v1
kind: Service

metadata:
  name: api
  namespace: cka-lab

spec:
  selector:
    app: api

  ports:
    - port: 80
      targetPort: 8000

Deployment 負責管理 Pod

第一個 Resource 是 Deployment:

apiVersion: apps/v1
kind: Deployment

Deployment 的主要工作不是直接執行 Application,而是管理 Pod 的期望狀態。

我們設定:

spec:
  replicas: 2

表示希望 Kubernetes 維持兩個 FastAPI Pod。

背後的關係是:

Deployment
    ↓
ReplicaSet
    ↓
2 個 Pod

如果其中一個 Pod 因故消失,ReplicaSet 會發現實際 Pod 數量只剩一個,接著自動建立新的 Pod,讓實際狀態回到我們要求的兩個。


Selector 與 Label 必須一致

Deployment 中有兩個重要設定:

selector:
  matchLabels:
    app: api

以及:

template:
  metadata:
    labels:
      app: api

template.metadata.labels 會放到 Deployment 建立的每個 Pod 上。

因此 Pod 會擁有:

app=api

這個 Label。

Deployment 的 Selector 則表示:

我要管理 Label 為 app=api 的 Pod

所以這兩個地方必須一致:

Deployment Selector
        ↓
      app=api
        ↑
Pod Template Label

如果不一致,Deployment 就無法正確管理自己建立的 Pod,Kubernetes 通常也會拒絕這份設定。


Container 使用自己建立的 Image

Container 設定如下:

containers:
  - name: api
    image: cka-api:v1

這代表 Pod 中會啟動一個名為:

api

的 Container,並使用:

cka-api:v1

這個 Image。

當 Container 啟動時,就會執行 Dockerfile 裡的:

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

因此 FastAPI 最後會在 Container 的 Port 8000 提供服務。
(複習!!
RUN 是 Image 建立時執行
CMD 則是容器啟動時運行
)


為什麼設定 imagePullPolicy: IfNotPresent

imagePullPolicy: IfNotPresent

這表示:

如果 Node 本機已經有這個 Image,就直接使用
只有找不到時才嘗試下載

我們已經透過:

kind load docker-image cka-api:v1 --name cka-lab

將 Image 放進 kind Node,因此 Kubernetes 應該直接使用 Node 裡的 Image。

如果使用本機 Image,卻設定成:

imagePullPolicy: Always

Kubernetes 每次建立 Container 時都會嘗試從外部 Registry 下載 Image。由於 cka-api:v1 沒有上傳到 Registry,Pod 就可能啟動失敗。

在正式環境中,Image 通常會先推送到 Container Registry,例如:

Docker Hub
GitHub Container Registry
Amazon ECR
Google Artifact Registry

Kubernetes Node 再從 Registry 拉取 Image。但目前使用 kind 本機練習,因此直接使用 kind load docker-image 即可。


containerPort 的作用

ports:
  - containerPort: 8000

這表示 Application 預期在 Container 的 Port 8000 提供服務。

它與 Dockerfile 裡的:

EXPOSE 8000

概念相似,主要是描述 Container 使用的 Port,讓設定與閱讀者更容易理解。

但要注意:

containerPort 本身不會建立 Service
也不會將 Port 開放到 Mac

真正負責讓其他 Pod 或 Service 將流量送進來的,仍然是後面的 Kubernetes Service。


Service 如何找到 Pod?

第二個 Resource 是 Service:

apiVersion: v1
kind: Service

Service 的名稱是:

metadata:
  name: api

它的 Selector 是:

selector:
  app: api

這表示 Service 會尋找具有以下 Label 的 Pod:

app=api

Deployment 建立的 Pod 剛好也有:

labels:
  app: api

因此 Service 可以找到這些 FastAPI Pod。

整個對應關係如下:

Service Selector
    app=api
       ↓
Pod Label
    app=api

Service 不會透過 Deployment 名稱尋找 Pod,也不是因為兩者都叫 api 才互相連接。真正建立關聯的是:

Selector 與 Label

即使 Deployment 改名,只要 Pod Label 仍然符合 Service Selector,Service 依然可以將流量送到 Pod。


Service Port 與 Container Port

Service 的 Port 設定如下:

ports:
  - port: 80 (自己對外提供的 Port 號,也就是別人會從 80 port 進來)
    targetPort: 8000 (Service 收到流量後,要把它轉送到 Pod 的 8000 Port)

其中:

port: 80

代表 Service 自己對外提供的 Port。

targetPort: 8000

代表 Service 收到流量後,要將它轉送到 Pod 的 Port 8000。

因此請求流向是:

Service Port 80 (別人從80進來)
        ↓
Pod Port 8000 (Service送去給8000)
        ↓
Uvicorn
        ↓
FastAPI

這兩個 Port 不一定要相同。

Service 可以使用大家熟悉的 HTTP Port 80,Application 則繼續在 Container 裡使用 Port 8000。

因為沒有指定:

type:

所以 Service 預設類型是:

ClusterIP

ClusterIP Service 主要提供 Cluster 內部存取,Mac 瀏覽器不能直接連線,因此稍後會使用 kubectl port-forward


套用 Kubernetes YAML

如果前面曾經建立同名的 Nginx Deployment 與 Service,可以先刪除:

kubectl delete deployment api \
  -n cka-lab \
  --ignore-not-found
kubectl delete service api \
  -n cka-lab \
  --ignore-not-found

--ignore-not-found 表示如果 Resource 原本不存在,不要把它當成錯誤。

接著套用新的設定:

kubectl apply -f k8s/01-api.yaml

應該會看到類似結果:

deployment.apps/api created
service/api created

https://ithelp.ithome.com.tw/upload/images/20260908/20168537tUrKxJxAF8.png

如果 Namespace 還不存在,需要先建立:

kubectl create namespace cka-lab

然後再重新執行 kubectl apply


確認 Deployment 狀態

先查看 Deployment:

kubectl get deployments \
  -n cka-lab

會看到:
https://ithelp.ithome.com.tw/upload/images/20260908/20168537kaT5eEmG48.png

其中:

READY 2/2

表示我們要求兩個 Replica,目前兩個都已經 Ready。

也可以等待 Deployment 完成更新:

kubectl rollout status deployment/api \
  -n cka-lab

成功時會看到類似:

deployment "api" successfully rolled out

查看 ReplicaSet 與 Pod

查看 Deployment 建立的 ReplicaSet:

kubectl get replicasets \
  -n cka-lab

https://ithelp.ithome.com.tw/upload/images/20260908/20168537lvmsT4ZZBR.png
再查看 Pod:

kubectl get pods \
  -n cka-lab \
  -o wide

得到:

https://ithelp.ithome.com.tw/upload/images/20260908/20168537Lj7sueSdCC.png

因為 Deployment 設定:

replicas: 2

所以應該會看到兩個 Pod。

READY 顯示:

1/1

代表 Pod 裡有一個 Container,而且這個 Container 已經 Ready。

STATUS 顯示:

Running

代表 Container 已經啟動。


查看 Application Log

可以透過 Deployment 查看 FastAPI Log:

kubectl logs deployment/api \
  -n cka-lab

會看到 Uvicorn 的啟動資訊:

https://ithelp.ithome.com.tw/upload/images/20260908/20168537ql8GmwuE78.png
因為 Deployment 有兩個 Pod,這個指令通常會選擇其中一個 Pod 顯示 Log。

如果要分別查看 Pod,可以先取得名稱:

kubectl get pods -n cka-lab

再指定其中一個:

kubectl logs <Pod名稱> \
  -n cka-lab

確認 Service 與 Endpoint

查看 Service:

kubectl get service \
  -n cka-lab

得到:

https://ithelp.ithome.com.tw/upload/images/20260908/20168537RhD3DU4Vp2.png!

Service 類型是 ClusterIP,而且使用 Port 80。

接著可以查看 Service 找到的 Endpoint:

kubectl get endpoints api \
  -n cka-lab

正常情況下,應該可以看到兩個 Pod IP,以及它們使用的 Port 8000:

https://ithelp.ithome.com.tw/upload/images/20260908/20168537eJq3EmL4Hf.png

這代表 Service 已經透過 Selector 找到兩個 FastAPI Pod。

如果 ENDPOINTS 顯示:

<none>

通常要檢查 Service Selector 與 Pod Label 是否一致:

kubectl get pods \
  -n cka-lab \
  --show-labels

Service 尋找的是:

app=api

因此 Pod 也必須擁有相同 Label。


從自己的電腦訪問 Service

目前 api 是 ClusterIP Service,只能直接在 Kubernetes Cluster 內部使用。

為了從 Mac 存取它,我們可以執行:

kubectl port-forward \
  service/api \
  8080:80 \
  -n cka-lab

這裡的:

8080:80

意思是:

Mac localhost:8080
        ↓
Kubernetes Service api:80

接著 Service 再根據自己的設定,把流量送到:

Pod:8000

完整流程如下:

Mac localhost:8080
        ↓
kubectl port-forward
        ↓
Service api:80
        ↓
Service Selector:app=api
        ↓
其中一個 FastAPI Pod:8000
        ↓
Uvicorn
        ↓
FastAPI

執行 port-forward 後,這個 Terminal 需要保持開啟。

正常情況下會看到:

Forwarding from 127.0.0.1:8080 -> 8000

https://ithelp.ithome.com.tw/upload/images/20260908/20168537NfpTNUlJsp.png!

雖然指令指定的是 Service Port 80,但 kubectl 最後會根據 Service 的 targetPort,將請求導向 Pod 的 Port 8000。


測試首頁 API

開啟另一個 Terminal:

curl http://localhost:8080

得到:

https://ithelp.ithome.com.tw/upload/images/20260908/20168537Aoy4JuBQu8.png

也可以直接使用瀏覽器開啟:

http://localhost:8080

FastAPI 會回傳:

{
  "message": "Hello from Kubernetes"
}

測試健康檢查 API

接著測試:

curl http://localhost:8080/health/live

得到:

{"status":"alive"}

這表示請求已經成功經過:

Mac
↓
port-forward
↓
Kubernetes Service
↓
FastAPI Pod
↓
Uvicorn
↓
FastAPI Endpoint

查看 FastAPI 自動產生的 API 文件

FastAPI 會自動產生互動式 API 文件。

保持 port-forward 執行,然後在瀏覽器開啟:

http://localhost:8080/docs

https://ithelp.ithome.com.tw/upload/images/20260908/20168537dt9k5qF86C.png

你會看到 Swagger UI,其中包含目前建立的兩個 API:

GET /
GET /health/live

也可以直接在頁面中送出 Request。

另一份 API 文件則位於:

http://localhost:8080/redoc

https://ithelp.ithome.com.tw/upload/images/20260908/20168537A7JrfiutGq.png

這些文件都是 FastAPI 根據程式中的路由自動產生的。


整理常見問題 Debug:

1. Pod 出現 ImagePullBackOff

如果執行:

kubectl get pods -n cka-lab

發現 Pod 狀態是:

ImagePullBackOff

或:

ErrImagePull

可以先查看詳細事件:

kubectl describe pod <Pod名稱> \
  -n cka-lab

最常見原因是忘記將 Image 載入 kind:

kind load docker-image \
  cka-api:v1 \
  --name cka-lab

載入後,可以刪除失敗的 Pod,讓 Deployment 自動建立新的:

kubectl delete pod <Pod名稱> \
  -n cka-lab

不需要手動建立 Pod。Deployment 與 ReplicaSet 會發現 Pod 數量不足,自動補上一個新的。


2. 修改程式後結果沒有改變

假設修改了:

return {
    "message": "Hello v2"
}

只修改程式碼不會自動更新正在執行的 Pod。

完整更新流程應該是:

修改 main.py
        ↓
重新建立 Docker Image
        ↓
將新 Image 載入 kind
        ↓
讓 Kubernetes 建立使用新 Image 的 Pod

開發時最好不要一直重複使用相同 Tag,否則可能難以判斷 Node 使用的是新 Image 還是舊 Image。

例如可以建立:

docker build \
  -t cka-api:v2 \
  ./app

載入 kind:

kind load docker-image \
  cka-api:v2 \
  --name cka-lab

接著把 YAML 裡的 Image 改成:

image: cka-api:v2

重新套用:

kubectl apply -f k8s/01-api.yaml

這時 Deployment 發現 Pod Template 已經改變,就會進行 Rolling Update。

也可以直接使用指令更新 Image:

kubectl set image deployment/api \
  api=cka-api:v2 \
  -n cka-lab

再觀察更新狀態:

kubectl rollout status deployment/api \
  -n cka-lab

3. Service 沒有回應

如果 Pod 是 Running,但是 Service 無法連線,可以按照以下順序檢查。

先確認 Pod Label:

kubectl get pods \
  -n cka-lab \
  --show-labels

確認其中包含:

app=api

再檢查 Service Selector:

kubectl describe service api \
  -n cka-lab

確認 Selector 也是:

app=api

接著檢查 Endpoint:

kubectl get endpoints api \
  -n cka-lab

如果沒有 Endpoint,通常代表 Service Selector 沒有匹配到任何 Pod。

如果有 Endpoint,還要確認 Application 是否真的監聽 Port 8000:

kubectl logs deployment/api \
  -n cka-lab

應該能看到:

Uvicorn running on http://0.0.0.0:8000

今天真正完成了什麼?

前面幾天,我們部署的是別人已經準備好的 Nginx Image。今天則是第一次將自己撰寫的程式,完整送進 Kubernetes。

你完成的不是單純執行幾個指令,而是走過整條 Application Delivery Path:

Python Source Code
        ↓
requirements.txt 定義相依套件
        ↓
Dockerfile 定義執行環境
        ↓
docker build 建立 Image
        ↓
kind load 將 Image 放進 Node
        ↓
Deployment 定義期望狀態
        ↓
ReplicaSet 維持兩個 Replica
        ↓
Pod 啟動 FastAPI Container
        ↓
Service 找到 app=api 的 Pod
        ↓
port-forward 連接 Mac 與 Service
        ↓
瀏覽器或 curl 收到 JSON Response

更重要的是,現在你應該能區分每一層的責任:

FastAPI
負責 Application 邏輯

Uvicorn
負責監聽 HTTP Port

Docker Image
負責封裝程式與執行環境

Container
負責執行 Image

Pod
負責在 Kubernetes 中承載 Container

Deployment
負責管理 Replica 與更新

Service
負責提供穩定入口並尋找 Pod

port-forward
負責暫時讓本機連入 Cluster

如果未來服務發生問題,我們就可以按照這些層次逐一判斷:

程式能不能執行?
Docker Container 能不能啟動?
Pod 是否為 Running?
Service 是否找到 Endpoint?
Port 是否正確對應?

這就是為什麼我們前面先用 Nginx 學 Kubernetes,再從今天開始加入自己的 Application。

今天不只是把 FastAPI 放進 Kubernetes,而是第一次真正打通:

從原始碼到 Kubernetes 服務

的完整流程。


上一篇
Day 9|Service 與 DNS:Pod IP 一直變,其他服務到底怎麼找到它?
系列文
不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言